<html>
 <head>
  <meta charset="UTF-8">
 </head>
 <body>
  <h1 data-lake-id="j79Gd" id="j79Gd"><span data-lake-id="u596ce13a" id="u596ce13a">典型回答</span></h1>
  <p data-lake-id="ua51e1ae1" id="ua51e1ae1"><br></p>
  <p data-lake-id="uc077a50d" id="uc077a50d"><span data-lake-id="u0169c87d" id="u0169c87d">假如，有一笔订单，在10:00下单成功，超时时间是1小时，那么在11点的时候，支付成功了，这时候该如何处理？</span></p>
  <p data-lake-id="u4e51d6fc" id="u4e51d6fc"><span data-lake-id="u47b9fa24" id="u47b9fa24">​</span><br></p>
  <h3 data-lake-id="pfbsP" id="pfbsP"><span data-lake-id="ue0fbcc85" id="ue0fbcc85">明确终态</span></h3>
  <p data-lake-id="u92f5adbf" id="u92f5adbf"><br></p>
  <p data-lake-id="u02f0a5fb" id="u02f0a5fb"><span data-lake-id="u1c34b89d" id="u1c34b89d">这是一个比较常见的一个并发处理的问题，而且也是业务中比较常见的问题，一个简单的支付单的状态机如下：</span></p>
  <p data-lake-id="u37e8b0aa" id="u37e8b0aa"><span data-lake-id="ub90c565c" id="ub90c565c">​</span><br></p>
  <p data-lake-id="ucdd6b5ab" id="ucdd6b5ab"><br></p>
  <p data-lake-id="u5a649e66" id="u5a649e66"><span data-lake-id="u4d0f0b22" id="u4d0f0b22">在"支付中"的一笔支付单 ，是有可能推进到支付成功的状态，并且也可能推进到已取消的状态的。</span></p>
  <p data-lake-id="ud03bb289" id="ud03bb289"><span data-lake-id="u5e50cb6c" id="u5e50cb6c">​</span><br></p>
  <p data-lake-id="ude82bcbb" id="ude82bcbb"><strong><u><span data-lake-id="u8a7de59d" id="u8a7de59d">一般来说，正常的支付业务中，支付成功和已取消，都应该是终态，也就是状态机中的最终状态，终态是不能再变化的。 如果一个模型没有明确的终态，或者已经终态的终态数据状态还能随便变化，那么他的设计一定是不合理的。</span></u></strong></p>
  <p data-lake-id="u02fb045f" id="u02fb045f"><span data-lake-id="ua74d69d3" id="ua74d69d3">​</span><br></p>
  <h3 data-lake-id="BAVSo" id="BAVSo"><span data-lake-id="ud2b99873" id="ud2b99873">状态流转控制</span></h3>
  <p data-lake-id="u572880c3" id="u572880c3"><br></p>
  <p data-lake-id="u27afe089" id="u27afe089"><span data-lake-id="u94d59837" id="u94d59837">那么，如果刚好在同一时刻，同一笔支付单同时来了一个支付成功的消息，和一个超时关闭的请求，该如何处理呢？</span></p>
  <p data-lake-id="ub13f81e8" id="ub13f81e8"><span data-lake-id="uab28987a" id="uab28987a">​</span><br></p>
  <p data-lake-id="u758df0ad" id="u758df0ad"><span data-lake-id="u52639c75" id="u52639c75">首先，我们要做的就是</span><strong><span data-lake-id="ub43ba52d" id="ub43ba52d">状态机的校验</span></strong><span data-lake-id="ubd2bc126" id="ubd2bc126">，在我们的支付成功处理和超时关闭的处理过程中，需要做状态的判断，只有支付中的状态才能执行这两个动作。并且数据库的update语句也需要做控制，即：</span></p>
  <p data-lake-id="u4e600802" id="u4e600802"><br></p>
  <pre lang="java"><code>
update pay_order set status = "PAY_SUCCESS",lock_version = lock_version + 1 where pay_order_no = #{parOrderNo} and status = "PAYING" and lock_version  = #{lock_version}


update pay_order set status = "PAY_EXPIRED",lock_version = lock_version + 1 where pay_order_no = #{parOrderNo} and status = "PAYING" and lock_version  = #{lock_version}
</code></pre>
  <p data-lake-id="uc1b7ce78" id="uc1b7ce78"><br></p>
  <p data-lake-id="ubef67ddc" id="ubef67ddc"><span data-lake-id="u807b6c71" id="u807b6c71">这时候在发生并发时，就可以确保一个pay_order，要么被推进到PAY_SUCCESS状态，要么被推进到PAY_EXPIRED状态，会且只会发生其中的一种情况。</span></p>
  <p data-lake-id="uae3a07a6" id="uae3a07a6"><span data-lake-id="ua3317e74" id="ua3317e74">​</span><br></p>
  <p data-lake-id="u1102ce04" id="u1102ce04"><span data-lake-id="ufe327cf7" id="ufe327cf7">那有一个成功了，就有一个会失败。这是必然的，这时候就有两种情况了：</span></p>
  <p data-lake-id="ub3dbb059" id="ub3dbb059"><span data-lake-id="ucaac024a" id="ucaac024a">​</span><br></p>
  <p data-lake-id="u974acbad" id="u974acbad"><span data-lake-id="u69acc64a" id="u69acc64a">1、支付成功处理成功，支付超时处理失败</span></p>
  <p data-lake-id="u41db4ca4" id="u41db4ca4"><span data-lake-id="ucdb47b93" id="ucdb47b93">2、支付超时处理成功，支付成功处理失败</span></p>
  <p data-lake-id="u6fe6bc1e" id="u6fe6bc1e"><span data-lake-id="u1d98243d" id="u1d98243d">​</span><br></p>
  <p data-lake-id="u7daffbcd" id="u7daffbcd"><span data-lake-id="udc643877" id="udc643877">这两种情况如何处理呢？</span></p>
  <p data-lake-id="ub0aab4cc" id="ub0aab4cc"><span data-lake-id="u57f243f2" id="u57f243f2">​</span><br></p>
  <h3 data-lake-id="uLwe1" id="uLwe1"><span data-lake-id="u38af15fe" id="u38af15fe">逆向流程</span></h3>
  <p data-lake-id="u6be07deb" id="u6be07deb"><br></p>
  <p data-lake-id="ue39161f5" id="ue39161f5"><span data-lake-id="u36f9ede6" id="u36f9ede6">先说简单的情况，假如支付成功处理成功，支付超时处理失败，这种其实没啥问题，因为已经支付成功了，超时的请求直接拒绝掉就行了。这是业务上正常的逻辑。</span></p>
  <p data-lake-id="u938024da" id="u938024da"><span data-lake-id="u3f587732" id="u3f587732">​</span><br></p>
  <p data-lake-id="u1b14d570" id="u1b14d570"><span data-lake-id="u5b22f643" id="u5b22f643">第二种情况就不好处理了，因为对于支付超时处理成功了，但是支付成功处理失败这种，我们就需要考虑，钱怎么办？</span></p>
  <p data-lake-id="u292b638e" id="u292b638e"><span data-lake-id="u33a7b1ac" id="u33a7b1ac">​</span><br></p>
  <p data-lake-id="uf7fe4796" id="uf7fe4796"><span data-lake-id="u200f2306" id="u200f2306">用户把钱付完了，但是支付却没成功，这肯定是业务上接受不了的。那这时候怎么办呢？</span></p>
  <p data-lake-id="u554251a4" id="u554251a4"><span data-lake-id="u5cbab4f0" id="u5cbab4f0">​</span><br></p>
  <p data-lake-id="u7f78813b" id="u7f78813b"><span data-lake-id="uaccc6aa3" id="uaccc6aa3">办法就是：</span><strong><span data-lake-id="uc94513da" id="uc94513da">原路退回</span></strong></p>
  <p data-lake-id="u11ab2fbd" id="u11ab2fbd"><span data-lake-id="ub4941758" id="ub4941758">​</span><br></p>
  <p data-lake-id="ubb3f1fc0" id="ubb3f1fc0"><span data-lake-id="u98c683a0" id="u98c683a0">当出现这种情况的时候，我们是可以识别出来的，也就是说在支付成功的处理过程中，如果发现支付单被关闭了，那么就触发原路退回的流程，把钱再给用户退回去。</span></p>
  <p data-lake-id="uebca7640" id="uebca7640"><span data-lake-id="u7bda83f8" id="u7bda83f8">​</span><br></p>
  <p data-lake-id="u5ae81104" id="u5ae81104"><span data-lake-id="u95df9899" id="u95df9899">并且这个过程需要考虑，如果退回失败了怎么办？</span></p>
  <p data-lake-id="ub5121c2f" id="ub5121c2f"><span data-lake-id="u3279f8e7" id="u3279f8e7">​</span><br></p>
  <h3 data-lake-id="TkkeB" id="TkkeB"><span data-lake-id="ua09a8bcd" id="ua09a8bcd">资金恒等式</span></h3>
  <p data-lake-id="ub8371433" id="ub8371433"><span data-lake-id="u8929126d" id="u8929126d">​</span><br></p>
  <p data-lake-id="u93fd23a5" id="u93fd23a5"><span data-lake-id="u94400f48" id="u94400f48">为了保证方案的完整性，就需要在支付单上多记录一些金额，比如支付金额、冲退金额。当支付单的状态为PAY_SUCCESS时，需要满足以下</span><strong><span data-lake-id="u820b4d68" id="u820b4d68">恒等式</span></strong><span data-lake-id="uef1ba4b8" id="uef1ba4b8">：</span></p>
  <p data-lake-id="u7fca6689" id="u7fca6689"><span data-lake-id="u5575d88f" id="u5575d88f">​</span><br></p>
  <blockquote data-lake-id="uffc24a37" id="uffc24a37">
   <p data-lake-id="u940f68a4" id="u940f68a4"><span data-lake-id="ueee6b5ea" id="ueee6b5ea">支付金额 &gt; 0 ；冲退金额=0 </span></p>
  </blockquote>
  <p data-lake-id="u1ba61c0f" id="u1ba61c0f"><span data-lake-id="u3e3eb138" id="u3e3eb138">​</span><br></p>
  <p data-lake-id="u3834c9a6" id="u3834c9a6"><span data-lake-id="uc9ef2c05" id="uc9ef2c05">当支付单的状态为PAY_EXPIRED时，需要满足以下恒等式：</span></p>
  <p data-lake-id="ufefa869f" id="ufefa869f"><span data-lake-id="u90eb48bc" id="u90eb48bc">​</span><br></p>
  <blockquote data-lake-id="u8bc0c1e7" id="u8bc0c1e7">
   <p data-lake-id="uf092e193" id="uf092e193"><span data-lake-id="u758b3709" id="u758b3709">支付金额-冲退金额 = 0</span></p>
  </blockquote>
  <p data-lake-id="ud9910181" id="ud9910181"><br></p>
  <p data-lake-id="u19af5896" id="u19af5896"><span data-lake-id="uba674624" id="uba674624">只有这样，我们才能确保，这笔单子，要么用户付了钱，支付成功。要么支付失败，钱没付或者钱付了之后退回去了。</span></p>
  <p data-lake-id="ueb6d29b8" id="ueb6d29b8"><span data-lake-id="u73e20036" id="u73e20036">​</span><br></p>
  <p data-lake-id="uafb81295" id="uafb81295"><span data-lake-id="u8e30d6d4" id="u8e30d6d4">那我们就需要引入对账机制，不断的做资金恒等式的校验，发现不一致的时候，需要进行对齐操作，即退款重试。直到成功为止。</span></p>
  <p data-lake-id="u3debb742" id="u3debb742"><span data-lake-id="u96d30e0e" id="u96d30e0e">​</span><br></p>
  <h3 data-lake-id="pbAKK" id="pbAKK"><span data-lake-id="udb3bc9f0" id="udb3bc9f0">分布式锁</span></h3>
  <p data-lake-id="ua74508da" id="ua74508da"><br></p>
  <p data-lake-id="u878672f9" id="u878672f9"><span data-lake-id="ub9e5746e" id="ub9e5746e">上面的方案大致差不多了，如果再做得好一点的话，可以在进入支付成功/支付超时的业务逻辑操作之前，先抢一下这笔支付单的锁，在前面就把并发控制好。没抢到锁的就失败，下次重试。</span></p>
  <p data-lake-id="u24fa254a" id="u24fa254a"><span data-lake-id="ue4b0214b" id="ue4b0214b">​</span><br></p>
  <p data-lake-id="u60b552e0" id="u60b552e0"><span data-lake-id="u71c6b210" id="u71c6b210">这样可以大大的降低并发冲突，避免出现异常，及很多线上的报错。</span></p>
 </body>
</html>